/**
 * 
 */
package net.phoenix.repository;

import java.util.ArrayList;
import java.util.Iterator;

import javax.jcr.Item;
import javax.jcr.ItemNotFoundException;
import javax.jcr.Node;
import javax.jcr.Property;
import javax.jcr.RepositoryException;
import javax.jcr.Session;

import org.jaxen.DefaultNavigator;
import org.jaxen.XPath;

import org.jaxen.FunctionCallException;
import org.jaxen.JaxenConstants;
import org.jaxen.JaxenException;
import org.jaxen.NamedAccessNavigator;
import org.jaxen.UnsupportedAxisException;

/**
 * @author lixf
 * 
 */
public class JCRNavigator extends DefaultNavigator implements NamedAccessNavigator{

	private Session session;

	public JCRNavigator(Session session) {
		this.session = session;
	}

	/**
	 * 
	 */
	private static final long serialVersionUID = -1537293985492474861L;

	/**
	 * Get an iterator over all of this node's children.
	 * 
	 * @param contextNode
	 *            the context node for the child axis.
	 * @return a possibly-empty iterator (not null)
	 */
	public Iterator<?> getChildAxisIterator(Object contextNode) {
		Node node = (Node) contextNode;
		try {
			return node.getNodes();
		} catch (RepositoryException e) {
			throw new RepositoryRuntimeException(e);
		}

	}

	/**
	 * Get a (single-member) iterator over this node's parent.
	 * 
	 * @param contextNode
	 *            the context node for the parent axis
	 * @return a possibly-empty iterator (not null)
	 */
	public Iterator<?> getParentAxisIterator(Object contextNode) {
		Item item = (Item) contextNode;
		ArrayList<Item> items = new ArrayList<Item>();
		items.add(item);
		return items.iterator();

	}

	/**
	 * Return the XPath parent of the supplied DOM node. XPath has slightly
	 * different definition of parent than DOM does. In particular, the parent
	 * of an attribute is not null.
	 * 
	 * @param child
	 *            the child node
	 * 
	 * @return the parent of the specified node; or null if the node does not
	 *         have a parent
	 */
	public Object getParentNode(Object child) {
		Item node = (Item) child;
		try {
			return node.getParent();
		} catch (RepositoryException e) {
			throw new RepositoryRuntimeException(e);
		}
	}

	/**
	 * Get an iterator over all following siblings.
	 * 
	 * @param contextNode
	 *            the context node for the sibling iterator
	 * @return a possibly-empty iterator (not null)
	 */
	public Iterator<?> getFollowingSiblingAxisIterator(Object contextNode) {
		return ((InternalNode) contextNode).getFollowingSibling();
	}

	/**
	 * Get an iterator over all preceding siblings.
	 * 
	 * @param contextNode
	 *            the context node for the preceding sibling axis
	 * @return a possibly-empty iterator (not null)
	 */
	public Iterator<?> getPrecedingSiblingAxisIterator(Object contextNode) {
		return ((InternalNode) contextNode).getPrecedingSibling();
	}

	/**
	 * Get an iterator over all following nodes, depth-first.
	 * 
	 * @param contextNode
	 *            the context node for the following axis
	 * @return a possibly-empty iterator (not null)
	 */
	public Iterator<?> getFollowingAxisIterator(Object contextNode) {
		return ((InternalNode) contextNode).getFollowingSibling();
	}

	/**
	 * Get an iterator over all attributes.
	 * 
	 * @param contextNode
	 *            the context node for the attribute axis
	 * @return a possibly-empty iterator (not null)
	 */
	public Iterator<?> getAttributeAxisIterator(Object contextNode) {
		try {
			return ((Node) contextNode).getProperties();
		} catch (RepositoryException e) {
			throw new RepositoryRuntimeException(e);
		}
	}

	/**
	 * Get an iterator over all declared namespaces.
	 * 
	 * <p>
	 * Note: this iterator is not live: it takes a snapshot and that snapshot
	 * remains static during the life of the iterator (i.e. it won't reflect
	 * subsequent changes to the DOM).
	 * </p>
	 * 
	 * <p>
	 * In the event that the DOM is inconsistent; for instance a
	 * <code>pre:foo</code> element is declared by DOM to be in the
	 * http://www.a.com/ namespace but also has an
	 * <code>xmlns:pre="http://www.b.com"</code> attribute; then only one of the
	 * namespaces will be counted. This will be the intrinsic namespace of the
	 * <code>Element</code> or <code>Attr</code> object rather than the one
	 * provide by the contradictory namespace declaration attribute. In the
	 * event of a contradiction between two attributes on the same element--e.g.
	 * <code>pre:foo</code> in the http://www.a.com/ namespace and
	 * <code>pre:bar</code> in the http://www.b.com/ namespace--it is undefined
	 * which namespace will be returned.
	 * </p>
	 * 
	 * @param contextNode
	 *            the context node for the namespace axis
	 * @return a possibly-empty iterator (not null)
	 */
	public Iterator<?> getNamespaceAxisIterator(Object contextNode) {
		return JaxenConstants.EMPTY_ITERATOR;

	}

	/**
	 * Returns a parsed form of the given XPath string, which will be suitable
	 * for queries on DOM documents.
	 * 
	 * @param xpath
	 *            the XPath expression
	 * @return a parsed form of the given XPath string
	 * @throws net.phoenix.repository.query.xpath.SAXPathException
	 *             if the string is syntactically incorrect
	 */
	public XPath parseXPath(String xpath) throws JaxenException {
		return new JCRPath(xpath);
	}

	/**
	 * Get the top-level document node.
	 * 
	 * @param contextNode
	 *            any node in the document
	 * @return the root node
	 */
	public Object getDocumentNode(Object contextNode) {
		if (isDocument(contextNode))
			return contextNode;
		else
			try {
				return ((Node) contextNode).getSession().getRootNode();
			} catch (RepositoryException e) {
				throw new RepositoryRuntimeException(e);
			}
	}

	// Why are there separate methods for getElementNamespaceURI and
	// getAttributeNamespaceURI when they do exactly the same thing?
	// This should be combined in a future version.
	/**
	 * Get the namespace URI of an element.
	 * 
	 * @param element
	 *            the target node
	 * @return a string (possibly empty) if the node is an element, and null
	 *         otherwise
	 */
	public String getElementNamespaceUri(Object element) {

		return null;
	}

	/**
	 * Get the local name of an element.
	 * 
	 * @param element
	 *            the target node
	 * @return a string representing the unqualified local name if the node is
	 *         an element, or null otherwise
	 */
	public String getElementName(Object element) {
		if (isElement(element)) {
			try {
				return ((Node) element).getName();
			} catch (RepositoryException e) {
				throw new RepositoryRuntimeException(e);
			}
		}
		return null;
	}

	/**
	 * Get the qualified name of an element.
	 * 
	 * @param element
	 *            the target node
	 * @return a string representing the qualified (i.e. possibly prefixed) name
	 *         if the argument is an element, or null otherwise
	 */
	public String getElementQName(Object element) {

		return getElementName(element);
	}

	/**
	 * Get the namespace URI of an attribute.
	 * 
	 * @param attribute
	 *            the target node
	 * 
	 * @return the namespace name of the specified node
	 * 
	 */
	public String getAttributeNamespaceUri(Object attribute) {
		return null;
	}

	/**
	 * Get the local name of an attribute.
	 * 
	 * @param attribute
	 *            the target node
	 * @return a string representing the unqualified local name if the node is
	 *         an attribute, or null otherwise
	 */
	public String getAttributeName(Object attribute) {
		if (isAttribute(attribute)) {
			try {
				return ((Property) attribute).getName();
			} catch (RepositoryException e) {
				throw new RepositoryRuntimeException(e);
			}
		}
		return null;
	}

	/**
	 * Get the qualified name of an attribute.
	 * 
	 * @param attribute
	 *            the target node
	 * 
	 * @return a string representing the qualified (i.e. possibly prefixed) name
	 *         if the argument is an attribute, or null otherwise
	 */
	public String getAttributeQName(Object attribute) {
		return getAttributeName(attribute);
	}

	/**
	 * Test if a node is a top-level document.
	 * 
	 * @param object
	 *            the target node
	 * @return true if the node is the document root, false otherwise
	 */
	public boolean isDocument(Object object) {
		try {
			return (object instanceof Node) && (object.equals(this.session.getRootNode()));
		} catch (RepositoryException e) {
			throw new RepositoryRuntimeException(e);
		}
	}

	/**
	 * Test if a node is a namespace.
	 * 
	 * @param object
	 *            the target node
	 * @return true if the node is a namespace, false otherwise
	 */
	public boolean isNamespace(Object object) {
		return false;
	}

	/**
	 * Test if a node is an element.
	 * 
	 * @param object
	 *            the target node
	 * @return true if the node is an element, false otherwise
	 */
	public boolean isElement(Object object) {
		return (object instanceof Node);
	}

	/**
	 * Test if a node is an attribute. <code>xmlns</code> and
	 * <code>xmlns:pre</code> attributes do not count as attributes for the
	 * purposes of XPath.
	 * 
	 * @param object
	 *            the target node
	 * @return true if the node is an attribute, false otherwise
	 */
	public boolean isAttribute(Object object) {
		return (object instanceof Property);
	}

	/**
	 * Test if a node is a comment.
	 * 
	 * @param object
	 *            the target node
	 * @return true if the node is a comment, false otherwise
	 */
	public boolean isComment(Object object) {
		return false;
	}

	/**
	 * Test if a node is plain text.
	 * 
	 * @param object
	 *            the target node
	 * @return true if the node is a text node, false otherwise
	 */
	public boolean isText(Object object) {
		return false;
	}

	/**
	 * Test if a node is a processing instruction.
	 * 
	 * @param object
	 *            the target node
	 * @return true if the node is a processing instruction, false otherwise
	 */
	public boolean isProcessingInstruction(Object object) {
		return false;
	}

	/**
	 * Get the string value of an element node.
	 * 
	 * @param object
	 *            the target node
	 * @return the text inside the node and its descendants if the node is an
	 *         element, null otherwise
	 */
	public String getElementStringValue(Object object) {
		return null;
	}

	/**
	 * Get the string value of an attribute node.
	 * 
	 * @param object
	 *            the target node
	 * @return the text of the attribute value if the node is an attribute, null
	 *         otherwise
	 */
	public String getAttributeStringValue(Object object) {
		if (isAttribute(object))
			return ((Property) object).toString();
		else
			return null;
	}

	/**
	 * Get the string value of text.
	 * 
	 * @param object
	 *            the target node
	 * @return the string of text if the node is text, null otherwise
	 */
	public String getTextStringValue(Object object) {
		return null;
	}

	/**
	 * Get the string value of a comment node.
	 * 
	 * @param object
	 *            the target node
	 * @return the text of the comment if the node is a comment, null otherwise
	 */
	public String getCommentStringValue(Object object) {
		return null;
	}

	/**
	 * Get the string value of a namespace node.
	 * 
	 * @param object
	 *            the target node
	 * @return the namespace URI as a (possibly empty) string if the node is a
	 *         namespace node, null otherwise
	 */
	public String getNamespaceStringValue(Object object) {

		return null;
	}

	/**
	 * Get the prefix value of a namespace node.
	 * 
	 * @param object
	 *            the target node
	 * @return the namespace prefix a (possibly empty) string if the node is a
	 *         namespace node, null otherwise
	 */
	public String getNamespacePrefix(Object object) {

		return null;
	}

	/**
	 * Translate a namespace prefix to a URI.
	 * 
	 * @param prefix
	 *            the namespace prefix
	 * @param element
	 *            the namespace context
	 * @return the namespace URI bound to the prefix in the scope of
	 *         <code>element</code>; null if the prefix is not bound
	 */
	public String translateNamespacePrefixToUri(String prefix, Object element) {

		return null;
	}

	/**
	 * Use JAXP to load a namespace aware document from a given URI.
	 * 
	 * @param uri
	 *            the URI of the document to load
	 * @return the new W3C DOM Level 2 Document instance
	 * @throws FunctionCallException
	 *             containing a nested exception if a problem occurs trying to
	 *             parse the given document
	 * 
	 * @todo Possibly we could make the factory a thread local.
	 */
	public Object getDocument(String uri) throws FunctionCallException {
		try {
			return this.session.getRootNode();
		} catch (RepositoryException e) {
			throw new RepositoryRuntimeException(e);
		}

	}

	/**
	 * Get the target of a processing instruction node.
	 * 
	 * @param obj
	 *            the processing instruction
	 * @return the target of the processing instruction
	 * @throws ClassCastException
	 *             if obj is not a processing instruxtion
	 * 
	 */
	public String getProcessingInstructionTarget(Object obj) {

		throw new ClassCastException(obj + " is not a processing instruction");
	}

	/**
	 * Get the data of a processing instruction node.
	 * 
	 * @param obj
	 *            the processing instruction
	 * @return the target of the processing instruction
	 * @throws ClassCastException
	 *             if obj is not a processing instruxtion
	 * 
	 */
	public String getProcessingInstructionData(Object obj) {

		throw new ClassCastException(obj + " is not a processing instruction");
	}

	/**
	 * Returns the element whose ID is given by elementId. If no such element
	 * exists, returns null. Attributes with the name "ID" are not of type ID
	 * unless so defined. Attribute types are only known if when the parser
	 * understands DTD's or schemas that declare attributes of type ID. When
	 * JAXP is used, you must call <code>setValidating(true)</code> on the
	 * DocumentBuilderFactory.
	 * 
	 * @param object
	 *            a node from the document in which to look for the id
	 * @param elementId
	 *            id to look for
	 * 
	 * @return element whose ID is given by elementId, or null if no such
	 *         element exists in the document or if the implementation does not
	 *         know about attribute types
	 * @see javax.xml.parsers.DocumentBuilderFactory
	 * 
	 * @throws ClassCastException
	 *             if object is not an <code>org.w3c.dom.Node</code> object
	 * 
	 */
	public Object getElementById(Object object, String elementId) {

		try {
			return session.getNodeByIdentifier(elementId);
		} catch (ItemNotFoundException e) {
			return null;
		} catch (RepositoryException e) {
			throw new RepositoryRuntimeException(e);
		}

	}

	@Override
	public Iterator<?> getChildAxisIterator(Object contextNode, String localName, String namespacePrefix, String namespaceURI) throws UnsupportedAxisException {
		Node node = (Node) contextNode;
		try {
			return node.getNodes(localName);
		} catch (RepositoryException e) {
			throw new RepositoryRuntimeException(e);
		}
	}

	@Override
	public Iterator<?> getAttributeAxisIterator(Object contextNode, String localName, String namespacePrefix, String namespaceURI) throws UnsupportedAxisException {
		try {
			return ((Node) contextNode).getProperties(localName);
		} catch (RepositoryException e) {
			throw new RepositoryRuntimeException(e);
		}
	}

}
